package com.project.thisDatabase.elasticsearch;

import com.google.gson.reflect.TypeToken;
import com.project.annotation.enums.elasticsearch.OperType;
import com.project.annotation.enums.elasticsearch.SearchType;
import com.project.annotation.enums.elasticsearch.SortConst;
import com.project.thisCommon.common.CommonFunction;
import com.project.thisCommon.common.ConvertEntity;
import com.project.thisDatabase.elasticsearch.convert.QueryConvert;
import com.project.thisDatabase.elasticsearch.model.*;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

@ConditionalOnProperty(name = "spring.data.elasticsearch.enable", havingValue = "true")
@Component
public class ElasticExecutor {

    final String type = "_doc";

    @Autowired(required = false)
    RestHighLevelClient restHighLevelClient;

    @Value("${spring.data.elasticsearch.shards:}")
    Integer number_of_shards;

    @Value("${spring.data.elasticsearch.replicas:}")
    Integer number_of_replicas;

    @Value("${spring.data.elasticsearch.batch:}")
    Integer batch_size;

    public void createIndex() throws Exception {
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourcePatternResolver.getResources("classpath*:idx/*.json");
        for (Resource res : resources) {
            String name = res.getFilename().substring(0, res.getFilename().lastIndexOf("."));
            String json = IOUtils.toString(res.getInputStream());
            createIndex(name, json);
        }
    }

    /**
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:30
     * @param idxName   索引名称
     * @param idxSQL    索引描述
     * @return void
     * @throws
     * @since
     */
    public boolean createIndex(String idxName,String idxSQL) throws Exception {
        if (this.indexExist(idxName)) {
            return false;
        }
        CreateIndexRequest request = new CreateIndexRequest(idxName);
        buildSetting(request);
        request.mapping(idxSQL, XContentType.JSON);
        CreateIndexResponse res = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
        if (!res.isAcknowledged()) {
            return false;
        }
        return true;
    }

    /** 断某个index是否存在
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:27
     * @param idxName index名
     * @return boolean
     * @throws
     * @since
     */
    public boolean indexExist(String idxName) throws Exception {
        GetIndexRequest request = new GetIndexRequest(idxName);
//        request.local(false);
//        request.humanReadable(true);
//        request.includeDefaults(false);
//        request.indicesOptions(IndicesOptions.lenientExpandOpen());
        return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
    }

    /** 设置分片
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 19:27
     * @param request
     * @return void
     * @throws
     * @since
     */
    public void buildSetting(CreateIndexRequest request){
        request.settings(Settings.builder().put("index.number_of_shards", number_of_shards)
                .put("index.number_of_replicas",number_of_replicas));
    }
    /**
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:27
     * @param idxName index
     * @param entity    对象
     * @return void
     * @throws
     * @since
     */
    public <T extends ElasticContent>  void insertOrUpdateOne(String idxName, ElasticBaseEntity<T> entity) throws IOException {
        IndexRequest request = new IndexRequest(idxName);
        request.id(entity.getId());
        XContentBuilder builder = bindQueryBuilder(entity.getBody());
        request.source(builder);
        restHighLevelClient.index(request, RequestOptions.DEFAULT);
    }

    public <T extends ElasticContent> T insert(String idxName, T entity) throws IOException {
        ElasticBaseEntity elasticBaseEntity = new ElasticBaseEntity();
        entity.setId(CommonFunction.CreateUUIDString());
        elasticBaseEntity.setBody(entity);
        insertOrUpdateOne(idxName, elasticBaseEntity);
        return entity;
    }

    public <T> XContentBuilder bindQueryBuilder(T entity) throws IOException {
        XContentBuilder builder = XContentFactory.jsonBuilder();
        Map en = ConvertEntity.CovertToMap(entity);
        builder.startObject();
        convertMapToBuilder(en, builder);
        builder.endObject();
        return builder;
    }

    protected void convertMapToBuilder(Map<String, Object> map, XContentBuilder builder) throws IOException {
        for(String key : map.keySet()){
            if (map.get(key) instanceof Map){
                builder.startObject(key);
                convertMapToBuilder((Map)map.get(key), builder);
                builder.endObject();
            } else if (map.get(key) instanceof List){
                builder.startArray(key);
                convertListToBuilder(key, (List)map.get(key), builder);
                builder.endArray();
            } else {
                builder.field(key, map.get(key));
            }
        }
    }

    protected void convertListToBuilder(String key, List list, XContentBuilder builder) throws IOException {
        for (Object m : list){
            if (m instanceof Map){
                builder.map((Map)m);
//                convertMapToBuilder((Map)m, builder);
            } else if (m instanceof List){
                convertListToBuilder(key, (List)m, builder);
            } else {
                builder.value(m);
            }
        }
    }


    /** 批量插入数据
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:26
     * @param idxName index
     * @param list 带插入列表
     * @return void
     * @throws
     * @since
     */
    public void insertBatch(String idxName, List<ElasticBaseEntity> list) throws IOException {
        BulkRequest request = new BulkRequest();
        list.forEach(item -> request.add(new IndexRequest(idxName).id(item.getId())
                .source(item.getBody(), XContentType.JSON)));
        restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
    }

    /** 批量删除
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:14
     * @param idxName index
     * @param idList    待删除列表
     * @return void
     * @throws
     * @since
     */
    public <T> void deleteBatch(String idxName, Collection<T> idList) throws IOException {
        BulkRequest request = new BulkRequest();
        idList.forEach(item -> request.add(new DeleteRequest(idxName, type, item.toString())));
        restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
    }

    /**
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:14
     * @param idxName index
     * @param builder   查询参数
     * @param c 结果类对象
     * @return java.util.List<T>
     * @throws
     * @since
     */
    public <T> ElasticResult<T> search(String idxName, SearchSourceBuilder builder, Class<T> c) throws IOException {
        SearchRequest request = new SearchRequest(idxName);
        request.source(builder);
        request.searchType("dfs_query_then_fetch");
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        SearchHits search = response.getHits();
        long total = search.getTotalHits().value;
        SearchHit[] hits = search.getHits();
        List<T> res = new ArrayList<>(hits.length);
        for (SearchHit hit : hits) {
            res.add(ConvertEntity.jsonToJava(hit.getSourceAsString(), c));
        }
        ElasticResult<T> resultBase = new ElasticResult<>();
        resultBase.setTotle(total);
        resultBase.setRows(res);
        return resultBase;
    }

    protected void createChild(AggerItem aggs, AggregationBuilder agg){
        for (AggerItem it : aggs.getChilds()) {
            AggregationBuilder aggregationBuilder = agg.subAggregation(AggregationBuilders.terms(it.getName()).field(it.getField()));
            createChild(it, aggregationBuilder);
        }
    }

    protected void readAgger(AggerItem item, Terms terms) {
        for (Terms.Bucket bucket : terms.getBuckets()){
            for (AggerItem it : item.getChilds()) {
                AggerItem sub = new AggerItem();
                Terms sub_terms = bucket.getAggregations().get(it.getName());
                sub.setName(it.getName());
                sub.setField(it.getField());
                sub.setKey(bucket.getKeyAsString());
                sub.setValue(bucket.getDocCount());
                item.getResults().add(sub);
                readAgger(it, sub_terms);
            }

            AggerItem agg = new AggerItem();
            agg.setName(item.getName());
            agg.setField(item.getField());
            agg.setKey(bucket.getKeyAsString());
            agg.setValue(bucket.getDocCount());
            item.getResults().add(agg);
        }
    }

    public <T extends QueryBase> List<AggerItem> aggreCount(String idxName, String path, T builder) throws Exception {
        String json = CommonFunction.ReadFileAll(path, "utf-8");
        Type tp = (new TypeToken<List<AggerSetting>>() {
        }).getType();
        List<AggerSetting> setting = ConvertEntity.jsonToJava(json, tp);
        List<AggerItem> list = new ArrayList<>();
        for (AggerSetting agg : setting){
            AggerItem item = new AggerItem();
            item.setName(agg.getName());
            item.setField(agg.getField());
            for (String s : agg.getChild()){
                AggerItem sub = new AggerItem();
                sub.setName(s);
                sub.setField(s);
                item.getChilds().add(sub);
            }

            list.add(item);
        }

        return aggreCount(idxName, list, builder);
    }

    public <T extends QueryBase> List<AggerItem> aggreCount(String idxName, List<AggerItem> aggItems, T builder) throws Exception {
        Class<?> b = builder.getClass();
        QueryConvert convert = new QueryConvert();
        ConvertEntity.copyEntity(builder, b, convert);
        SearchSourceBuilder query = convert.builder();
        return aggreCount(idxName, aggItems, query);
    }

    protected List<AggerItem> aggreCount(String idxName, List<AggerItem> aggItems, SearchSourceBuilder query) throws Exception {
        SearchRequest request = new SearchRequest(idxName);
        for (AggerItem agg : aggItems){
            AggregationBuilder build = AggregationBuilders.terms(agg.getName()).field(agg.getField()).size(100);
            createChild(agg, build);
            query.aggregation(build).size(100);
        }
        request.source(query);
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        for (AggerItem agg : aggItems){
            Terms terms = response.getAggregations().get(agg.getName());
            readAgger(agg, terms);
        }

        return aggItems;
    }

    public <T extends QueryBase> List<AggerItem> aggreCount(String idxName, String path,  MultiQuery<T> builder) throws Exception {
        String json = CommonFunction.ReadFileAll(path, "utf-8");
        Type tp = (new TypeToken<List<AggerSetting>>() {
        }).getType();
        List<AggerSetting> setting = ConvertEntity.jsonToJava(json, tp);
        List<AggerItem> list = new ArrayList<>();
        for (AggerSetting agg : setting){
            AggerItem item = new AggerItem();
            item.setName(agg.getName());
            item.setField(agg.getField());
            for (String s : agg.getChild()){
                AggerItem sub = new AggerItem();
                sub.setName(s);
                sub.setField(s);
                item.getChilds().add(sub);
            }

            list.add(item);
        }

        return aggreCount(idxName, list, createSearchBuilder(builder));
    }

    public TokensItem pretty(String type, List<String> strs) throws IOException {
        Request request = new Request("GET", "_analyze");
        Map<String, Object> mp = new TreeMap<>();
        mp.put("analyzer", type);
        mp.put("text", strs);
        String json = ConvertEntity.javaToJson(mp);
        request.setJsonEntity(json);

        Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
        TokensItem tokens = ConvertEntity.jsonToJava(EntityUtils.toString(response.getEntity()), TokensItem.class);

        return tokens;
    }

    public <T, V extends QueryBase> ElasticResult<T> search(String idxName, V builder, Class<T> c) throws Exception {
        Class<?> b = builder.getClass();
        QueryConvert query = new QueryConvert();
        ConvertEntity.copyEntity(builder, b, query);
        SearchSourceBuilder bd = query.builder();
        if (null != builder.getPages()){
            bd.from(builder.getPages().getFrom());
            bd.size(builder.getPages().getSize());
            if (null != builder.getPages().getSort()) {
                for (String k : builder.getPages().getSort().keySet()) {
                    bd.sort(k, "DESC".equalsIgnoreCase(builder.getPages().getSort().get(k)) ? SortOrder.DESC : SortOrder.ASC);
                }
                bd.sort("_score");
            }
        }

        return search(idxName, bd, c);
    }

    private <T extends QueryBase> SearchSourceBuilder createSearchBuilder(MultiQuery<T> builder) throws Exception {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        BoolQueryBuilder boolFilterBuilder = new BoolQueryBuilder();
        List<BoolQueryBuilder> nextQueryBuilder = new ArrayList<>();
        List<BoolQueryBuilder> nextFilterBuilder = new ArrayList<>();

        for (SearchListBase<T> it : builder.getItems()) {
            Class<?> b = it.getItem().getClass();
            QueryConvert query = new QueryConvert();
            ConvertEntity.copyEntity(it.getItem(), b, query);
            if (SearchType.QUERY.equals(it.getSearchType())) {
                if (OperType.MUST.equals(it.getOper())) {
                    boolQueryBuilder.must(query.boolQueryBuilder);
                } else if (OperType.SHOULD.equals(it.getOper())) {
                    boolQueryBuilder.should(query.boolQueryBuilder);
                } else if (OperType.NOT.equals(it.getOper())) {
                    boolQueryBuilder.mustNot(query.boolQueryBuilder);
                } else if (OperType.NEXT.equals(it.getOper())) {
                    nextQueryBuilder.add(query.boolQueryBuilder);
                }
            } else if (SearchType.FILTER.equals(it.getSearchType())) {
                if (OperType.MUST.equals(it.getOper())) {
                    boolFilterBuilder.must(query.boolFilterBuilder);
                } else if (OperType.SHOULD.equals(it.getOper())) {
                    boolFilterBuilder.should(query.boolFilterBuilder);
                } else if (OperType.NOT.equals(it.getOper())) {
                    boolFilterBuilder.mustNot(query.boolFilterBuilder);
                }  else if (OperType.NEXT.equals(it.getOper())) {
                    nextFilterBuilder.add(query.boolFilterBuilder);
                }
            }
        }

        BoolQueryBuilder q = boolQueryBuilder;
        for (BoolQueryBuilder next : nextQueryBuilder) {
            BoolQueryBuilder n = new BoolQueryBuilder();
            n.must(q);
            n.must(next);
            q = n;
        }

        BoolQueryBuilder f = boolFilterBuilder;
        for (BoolQueryBuilder next : nextFilterBuilder) {
            BoolQueryBuilder n = new BoolQueryBuilder();
            n.must(f);
            n.must(next);
            f = n;
        }

        searchSourceBuilder.query(q);
        searchSourceBuilder.postFilter(f);

        if (null != builder.getPages()){
            searchSourceBuilder.from(builder.getPages().getFrom());
            searchSourceBuilder.size(builder.getPages().getSize());
            if (null != builder.getPages().getSort()) {
                for (String k : builder.getPages().getSort().keySet()) {
                    if ("_score".equalsIgnoreCase(k)) {
                        searchSourceBuilder.sort("_score");
                    } else {
                        searchSourceBuilder.sort(k, SortConst.DESC.equalsIgnoreCase(builder.getPages().getSort().get(k)) ? SortOrder.DESC : SortOrder.ASC);
                    }
                }

            }
        }

        return searchSourceBuilder;
    }

    public <T, V extends QueryBase> ElasticResult<T> search(String idxName, MultiQuery<V> builder, Class<T> c) throws Exception {
        return search(idxName, createSearchBuilder(builder), c);
    }

    /** 删除index
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:13
     * @param idxName
     * @return void
     * @throws
     * @since
     */
    public boolean deleteIndex(String idxName) throws Exception {
        if (!this.indexExist(idxName)) {
            return false;
        }
        restHighLevelClient.indices().delete(new DeleteIndexRequest(idxName), RequestOptions.DEFAULT);
        return true;
    }


    /**
     * @author WCNGS@QQ.COM
     * @See
     * @date 2019/10/17 17:13
     * @param idxName
     * @param builder
     * @return void
     * @throws
     * @since
     */
    public void deleteByQuery(String idxName, QueryBuilder builder) throws IOException {
        DeleteByQueryRequest request = new DeleteByQueryRequest(idxName);
        request.setQuery(builder);
        //设置批量操作数量,最大为10000
        request.setBatchSize(batch_size);
        request.setConflicts("proceed");
        restHighLevelClient.deleteByQuery(request, RequestOptions.DEFAULT);
    }
}
